第 1 步 - 定义数据源

在本教程的这一步骤中,创建一个在其中定义数据源的 Kanzi Engine 插件。数据源用于提供数据给您的应用程序。

教程资产

本教程的起点资料存储在 <KanziWorkspace>/Tutorials/Data sources/Start 目录中:

<KanziWorkspace>/Tutorials/Data sources/Completed 目录包含本教程已完成的工程。

定义数据源

要定义数据源:

  1. 在 Visual Studio 中,打开 <KanziWorkspace>/Tutorials/Data sources/Start/Application/configs/platforms/win32/XML_data_source.sln Visual Studio 解决方案。

    如果您在 Visual Studio 2017 中打开教程解决方案,遇到提示您重新定位工程到最新的 Microsoft 工具集时,请点击取消 (Cancel)。

  2. 在 Windows 资源管理器中,从 <KanziWorkspace>/Tutorials/Data sources/Assets/TinyXML-2 目录复制 tinyxml2.cpptinyxml2.h 文件到 <KanziWorkspace>/Tutorials/Data sources/Start/Application/src/plugin/src 目录。
    在本教程中,您将使用 TinyXML-2 素材库来处理用作数据源的 XML 文件。
  3. 在 Visual Studio 的 Solution Explorer 中右键点击 XML_data_source 工程,选择添加 > 现有项 (Add > Existing Item),并在 <KanziWorkspace>/Tutorials/Data sources/Start/Application/src/plugin/src 目录中选择 tinyxml2.cpptinyxml2.h 文件,然后点击添加 (Add)。
  4. 在 Visual Studio 中,对 xmldatasource.hpp 文件做如下更改:
    1. XMLDataSource 类的公有部分创建一个属性类型,用于设置希望此数据源使用的 XML 文件。
      替换
      class XML_DATA_SOURCE_API XMLDataSource : public DataSource
      {
      public:
      
          KZ_METACLASS_BEGIN(XMLDataSource, DataSource, "CustomDataSourceType")
          KZ_METACLASS_END()
      
      ...
      
      };
      class XML_DATA_SOURCE_API XMLDataSource : public DataSource
      {
      public: 
          //创建 XmlFilename 属性类型。属性类型可用于告诉数据源插件要读取哪个 XML 文件。
          static PropertyType<string> XmlFilenameProperty;
      
          KZ_METACLASS_BEGIN(XMLDataSource, DataSource, "XML_data_source")
              //将您创建的属性添加到类元数据。
              KZ_METACLASS_PROPERTY_TYPE(XmlFilenameProperty)
          KZ_METACLASS_END()
      
      ...
      
      };
    2. initialize() 函数受保护的部分后声明:
      • 帮助函数 parseFile,用以解析用户选择的 XML 文件。
      • 加载应用程序 kzb 文件后,Kanzi 调用的函数
      class XML_DATA_SOURCE_API XMLDataSource : public DataSource
      {
      
      ...
      
      protected: 
      
      ...
          //声明解析 XML 文件的函数,并从其内容创建数据对象。
          void parseFile(vector<char> fileData);
      
          //加载应用程序 kzb 文件后,Kanzi 调用 onLoaded。
          virtual void onLoaded() KZ_OVERRIDE;
      ...
      };
  5. 在 Visual Studio 中,向 xmldatasource.cpp 文件添加:
    1. 包含处理 XML 所需的头文件。
      //提供处理 XML 的功能。
      #include "tinyxml2.h"
    2. 定义您在 xmldatasource.hpp 头文件中创建的属性类型的元数据。属性可用于告诉数据源插件从哪个 XML 文件中获取数据。
      //定义用于告诉数据源插件读取哪个 XML 文件的属性类型的元数据。
      PropertyType<string> XMLDataSource::XmlFilenameProperty(kzMakeFixedString("XMLdatasource.XMLDataSourceFile"), "", 0, false,
                                                              KZ_DECLARE_EDITOR_METADATA
                                                              (
                                                                  //设置属性名称在 Kanzi Studio 中显示的方式。
                                                                  metadata.displayName = "XML Data Source File";
                                                                  //设置属性的工具提示。
                                                                  metadata.tooltip = "Sets which XML file the data source plugin reads.";
                                                                  //选择用于编辑属性类型的值的编辑器。
                                                                  // BrowseFileTextEditor 编辑器含有一个文本框,旁边有一个浏览 (Browse) 按钮。
                                                                  metadata.editor = "BrowseFileTextEditor";
                                                              ));
    3. 在元数据定义的前面,创建一个函数,它会根据数据源文件的 XML 元素中的类型特性中指定的类型创建数据对象。
      //添加 XML 元素中的类型特性指定类型的数据对象。//从文本参数获取初始值。
      DataObjectSharedPtr addDataObject(Domain *domain, const char* type, const char* name, const char* text)
      {
          shared_ptr<DataObject> object;
          //从 int 类型特性创建整数数据对象。
          if (type && strcmp(type, "int") == 0)
          {
              int value = 0;
              if (text)
              {
                  value = atoi(text);
              }
              object = make_shared<DataObjectInt>(domain, name, value);
          }
          //从浮点和实数类型特性创建浮点数据对象。
          else if (type && (strcmp(type, "real") == 0 || strcmp(type, "float") == 0))
          {
              double value = 0;
              if (text)
              {
                  value = atof(text);
              }
              object = make_shared<DataObjectReal>(domain, name, value);
          }
          //从布尔和布尔值类型特性创建布尔值数据对象。
          else if (type && (strcmp(type, "bool") == 0 || strcmp(type, "boolean") == 0))
          {
              bool value = false;
              if (text)
              {
                  value = (strcmp(text, "true") == 0);
              }
              object = make_shared<DataObjectBool>(domain, name, value);
          }
          //从字符串类型特性创建一个字符串数据对象。
          else if (type && strcmp(type, "string") == 0)
          {
              string value;
              if (text)
              {
                  value = text;
              }
              object = make_shared<DataObjectString>(domain, name, value);
          }
          else
          {
              //如未设置类型特性,则创建通用数据对象。
              //用于创建数据源中的层级。
              object = make_shared<DataObject>(domain, name);
          }
          return object;
      }
    4. addDataObject 函数后面,创建一个函数,将内存中的 XML 结构内容转换为数据对象。
      //将内存中的 XML 结构内容转换为数据对象。使用这些数据对象来//构造数据源的数据对象树。
      //第二个参数设置通道放置新数据对象的节点。
      //第三个参数将指针设为正在进行变换和语法分析的位置(在内存中的 XML 内,XML 中的子元素中)。
      static void addDataObjectsRecursively(Domain* domain, DataObjectSharedPtr parent, const tinyxml2::XMLElement* xml)
      {
          //检查 XML 文件中的当前元素是否设置了类型特性。
          const tinyxml2::XMLAttribute* typeAttribute = xml->FindAttribute("type");
      
          //获取类型特性的值。
          const char* type = 0;
          if (typeAttribute)
          {
              type = typeAttribute->Value();
          }
      
          //根据类型特性的值创建数据对象。
          DataObjectSharedPtr object = addDataObject(domain, type, xml->Name(), xml->GetText());
          //将数据对象作为子对象添加到父数据对象。
          parent->addChild(object);
      
          //遍历 XML 文件中的树,为当前 XML 元素的每个子元素添加数据对象。
          for (const tinyxml2::XMLElement* child = xml->FirstChildElement(); child; child = child->NextSiblingElement())
          {
              //递归。
              addDataObjectsRecursively(domain, object, child);
          }
      }
    5. addDataObject 函数前面,创建一个函数将用户使用 XML Data Source File 属性设置的 XML 文件从硬盘加载到内存中。
      //声明 addDataObjectsRecursively 函数。
      static void addDataObjectsRecursively(Domain* domain, DataObjectSharedPtr object, const tinyxml2::XMLElement* xml);
      
      //解析 XML 文件并从其内容创建数据对象。
      void XMLDataSource::parseFile(vector<char> fileData)
      {
          //清除之前的数据对象树。
          m_root.reset();
      
          //从内存解析 XML 文档并释放开放文件。
          tinyxml2::XMLDocument doc;
          tinyxml2::XMLError error = doc.Parse(fileData.data(), fileData.size());
      
          //如果插件成功加载 XML Data Source File 属性中设置的文件,则创建数据对象。
          if (error == tinyxml2::XML_SUCCESS)
          {
              //获取根 XML 元素。
              const tinyxml2::XMLElement* element = doc.RootElement();
              //创建数据源的根数据对象。
              m_root = make_shared<DataObject>(getDomain(), "Root");
              do
              {
                  //填充根数据对象的子数据对象。
                  addDataObjectsRecursively(getDomain(), m_root, element);
                  // 处理根 XML 元素的所有兄弟元素。
                  element = element->NextSiblingElement();
              } while (element);
      
              notifyModified();
          }
      }
    6. parseFile 函数前面,创建一个函数,将用作数据源的文件内容读取进内存。
      // 将用作数据源的文件内容读取进内存。
      vector<char> readFileContents(string_view filename)
      {
          ReadOnlyDiskFile file(filename);
          vector<char> data(static_cast<size_t>(file.size()));
          file.read(data.data(), data.size());
      
          return data;
      }
    7. readFileContents 函数后面,执行加载应用程序 kzb 文件后 Kanzi 调用的函数。
      // Kanzi 在加载应用程序 kzb 文件后调用 onLoaded。
      void XMLDataSource::onLoaded()
      {
          // 如果不设置 XML Data Source File 属性值,插件将不会执行任何操作。
          string filename = getProperty(XmlFilenameProperty);
          if (!filename.empty())
          {
              // 调用将 XML 文件内容读取进内存的函数,并解析返回值 
              // 到解析 XML 文件的函数,并从其内容创建数据对象。
              parseFile(readFileContents(filename));
          }
      }
      

更新数据源

本节演示如何通过随时检查用作数据源的文件是否更改来更新数据源。

要更新数据源:

  1. xmldatasource.hpp 做如下更改:
    1. 在私有部分中引入用于跟踪数据源中更改内容的线程和函数。
      替换 (Replace)
      private: 
      
          //将指针引入数据源的根数据对象。
          DataObjectSharedPtr m_root;

      private: 
      
          //声明启动监控和读取 XML 文件的线程的函数。
          void startWorkerThread(string_view filename);
      
          //声明停止监控和读取 XML 文件的线程的函数。
          void stopWorkerThread();
      
          //声明在线程中被执行用于监控和读取 XML 文件的函数。
          //传递文件名的字段串副本,因为它被用于其他线程中。
          void workerThreadCallback(string filename);
      
          //引入指向数据源根数据对象的指针。
          DataObjectSharedPtr m_root;
      
          //引入用于监控和读取 XML 文件的线程。
          thread m_workerThread;
      
          //引入工人线程的退出标志。
          atomic<bool> m_workerThreadExitCondition;
    2. 在受保护部分,修改构造函数以初始化工人线程的退出标志。
      替换
          //构造函数。
          explicit XMLDataSource(Domain* domain, string_view name):
              DataSource(domain, name)
          {
          }
          //构造函数。
          explicit XMLDataSource(Domain* domain, string_view name):
              DataSource(domain, name),
              m_workerThreadExitCondition(false)
          {
          }
    3. 在公有部分 create 函数定义的后面,声明析构函数。
      public: 
      
      ...
          //板构函数
          ~XMLDataSource();
  2. xmldatasource.cpp 做如下更改:
    1. 包含监控用作数据源的文件的函数所使用的头文件。
      //提供系统实用工具函数来获取文件的时间戳。
      #include <sys/stat.h>
    2. parseFile 函数前面,创建监控和读取用作数据源的文件的函数。
      //辅助函数,用于获取文件修改时间。
      static time_t getFileModificationTime(const char* filename)
      {
          time_t result = 0;
      
          struct stat fs;
          if (stat(filename, &fs) == 0)
          {
              result = fs.st_mtime;
          }
      
          return result;
      }
      
      //启动监控和读取 XML 文件的线程。
      void XMLDataSource::startWorkerThread(string_view filename)
      {
          kzAssert(!m_workerThread.joinable());
      
          m_workerThread = thread(&XMLDataSource::workerThreadCallback, this, string(filename));
      }
      
      //停止监控和读取 XML 文件的线程。
      void XMLDataSource::stopWorkerThread()
      {
          m_workerThreadExitCondition = true;
      
          if (m_workerThread.joinable())
          {
              m_workerThread.join();
          }
      
          m_workerThreadExitCondition = false;
      }
      
      //此线程函数每秒钟检查一次 XML 文件是否被修改。
      //此函数读取 XML 文件并向 UI 线程提交一个任务,将文件内容
      //解析进数据源结构中。
      void XMLDataSource::workerThreadCallback(string filename)
      {
          TaskDispatcher* taskDispatcher = getDomain()->getTaskDispatcher();
      
          //初始化上一次对无效值的修改时间。
          time_t oldModificationTime = static_cast<time_t>(-1);
      
          for (;;)
          {
              if (!filename.empty())
              {
                  time_t newModificationTime = getFileModificationTime(filename.c_str());
      
                  if (oldModificationTime != newModificationTime)
                  {
                      //将文件内容读取进内存。
                      //在此线程上读取文件内容,避免阻止 UI 线程长期运行。
                      vector<char> data = readFileContents(filename);
      
                      //提交任务,解析 XML 文件并构造数据源。
                      //必须在 UI 线程中执行此操作,因为只有在 UI 线程中,才可以控制数据源对象
                      //和其他 UI 对象。
                      taskDispatcher->submit(bind(&XMLDataSource::parseFile, this, kanzi::move(data)));
      
                      oldModificationTime = newModificationTime;
                  }
              }
      
              //睡眠一秒钟。
              kzsThreadSleep(1000);
      
              //如果 UI 线程设有退出标志,则退出。
              if (m_workerThreadExitCondition)
              {
                  break;
              }
          }
      }
    3. 修改 onLoaded 函数,添加监控和读取 XML 文件的关闭和启动线程。
      // Kanzi 在加载应用程序 kzb 文件后调用 onLoaded。
      void XMLDataSource::onLoaded()
      {
          //每当用户编辑数据源属性或刷新数据源时,
          //Kanzi Studio 预览 (Preview) 可以多次调用 onLoaded 函数。
          //关闭监控和读取 XML 文件的线程。
          stopWorkerThread();
      
          ...
      
          if (!filename.empty())
          {
              ...
      
              //启动监控和读取 XML 文件的线程。
              startWorkerThread(filename);
          }
      }
      
    4. onLoaded 函数前面,定义析构函数。
      //析构函数
      XMLDataSource::~XMLDataSource()
      {
          //关闭监控和读取 XML 文件的线程。
          stopWorkerThread();
      }

构建数据源插件

创建插件之后,构建插件.dll,您可使用该插件在本教程中 Kanzi Studio 工程的下一步中,从 XML 文件获取应用程序数据。

要构建数据源插件,请执行以下操作:

  1. 为您的 Visual Studio 版本选择其中一个 DLL 解决方案配置。
    开发时选择其中一个调试 DLL 配置。准备好创建产品版本后,选择其中一个发布 DLL 配置。
    例如,选择 GL_vs2015_Release_DLL 配置。
  2. 在 Solution Explorer 中右键点击 XML_data_source 工程并选择“构建 (Build)”。

< 简介
下一步 >